package com.seventhdawn.gwt.rpc.scripttag.client;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.URL;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.InvocationException;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.impl.RemoteServiceProxy;
import com.google.gwt.user.client.rpc.impl.RpcStatsContext;
import com.google.gwt.user.client.rpc.impl.Serializer;
import com.google.gwt.user.client.rpc.impl.RequestCallbackAdapter.ResponseReader;

/**
 * Client-side {@link com.google.gwt.user.client.rpc.RemoteService
 * RemoteService} proxy superclass that uses {@code script} tags instead of
 * standard {@code XMLHttpRequest}s for cross-site RPC communication.
 * <p>
 * The proxy creates new {@code script} tag that points to remote service URL
 * for each RPC request. When attached to DOM, the {@code script} tag calls the
 * server and evaluates resulting data. The proxy expects the data as JSONP with
 * a callback function being fired upon evaluation. The callback function then
 * calls a handler that extracts the RPC payload from the JSON object. Resulting
 * RPC payload is deserialized and passed back to the caller via
 * {@link com.google.gwt.user.client.rpc.AsyncCallback AsyncCallback} interface.
 * <p>
 * Since {@code script} tags allow for arbitrary JavaScript code execution, care
 * should be taken in order to avoid cross-site scripting vulnerabilities on the
 * client. This generally means that you should communicate with trusted servers
 * only.
 * <p>
 * <b>Important:</b> When using this proxy superclass, be sure to apply
 * {@link com.seventhdawn.gwt.rpc.scripttag.server.ScriptTagFilter
 * ScriptTagFilter} <em>before your server-side RPC service implementation</em>
 * (e.g. a {@link com.google.gwt.user.server.rpc.RemoteServiceServlet
 * RemoteServiceServlet} subclass) takes over the RPC request processing.
 * <p>
 * <b>Warning:</b> Cross-site RPC calls using {@code script} tags might have
 * issues with URL length limitation of web browsers and web servers. In
 * general, the URL length should not exceed 2kB. Since the proxy puts the RPC
 * request payload into URL passed to {@code script} tag's {@code src}
 * attribute, users should make sure that resulting RPC payload is small enough
 * to fit the URL length limitation. This generally means that you should avoid
 * having complex objects in your remote service interface.
 * 
 * @author vojtech.szocs
 * @author Artem Kozarezov
 */
public class ScriptTagServiceProxy extends RemoteServiceProxy {

    /**
     * Name of the URL parameter that holds the RPC request payload.
     */
    public static final String URL_PARAM_REQUEST_PAYLOAD = "rpc";

    /**
     * Name of the URL parameter that holds the JSONP callback function name.
     */
    public static final String URL_PARAM_CALLBACK = "cb";

    /**
     * Name of the JSON object property that holds the RPC response payload.
     */
    public static final String JSON_PARAM_RESPONSE_PAYLOAD = "rpcResult";

    protected ScriptTagServiceProxy(String moduleBaseURL, String remoteServiceRelativePath, String serializationPolicyName, Serializer serializer) {
        super(moduleBaseURL, remoteServiceRelativePath, serializationPolicyName, serializer);
    }

    /**
     * Prepares the remote service URL containing RPC request payload.
     */
    private String getServiceUrl(String requestData) {
        String serviceEntryPoint = getServiceEntryPoint();

        StringBuilder url = new StringBuilder(serviceEntryPoint);
        url.append(serviceEntryPoint.indexOf("?") != -1 ? "&" : "?");
        url.append(URL.encodeQueryString(URL_PARAM_REQUEST_PAYLOAD));
        url.append("=");
        url.append(URL.encodeQueryString(requestData));

        return url.toString();
    }

    /**
     * Users wishing to customize JSONP request invoker can override this method
     * in their custom service proxies.
     */
    protected ScriptTagRequestInvoker createRequestInvoker(String serviceUrl, int requestId) {
        return new ScriptTagRequestInvoker(serviceUrl, requestId);
    }

    /**
     * Performs a remote service method invocation using a {@code script} tag.
     */
    @Override
    @SuppressWarnings(value = { "unchecked", "unused" })
    protected <T> Request doInvoke(final ResponseReader responseReader, final String methodName, final RpcStatsContext statsContext,
            final String requestData, final AsyncCallback<T> callback) {
        try {
            final ScriptTagRequestInvoker requestInvoker = createRequestInvoker(getServiceUrl(requestData), statsContext.getRequestId());
            requestInvoker.send(new ScriptTagResponseHandler() {

                public void onCompletion(JavaScriptObject json) {
                    T result = null;
                    Throwable caught = null;

                    try {
                        // extract RPC response payload from JSON object
                        JSONObject jsonObject = new JSONObject(json);
                        JSONValue rpcResult = jsonObject.get(JSON_PARAM_RESPONSE_PAYLOAD);
                        String encodedResponse = URL.decodeQueryString(rpcResult.isString().stringValue());

                        boolean toss = statsContext.isStatsAvailable()
                                && statsContext.stats(statsContext.bytesStat(methodName, encodedResponse.length(), "responseReceived"));

                        if (encodedResponse == null) {
                            caught = new InvocationException("No response payload");
                        } else if (isReturnValue(encodedResponse)) {
                            result = (T) responseReader.read(createStreamReader(encodedResponse));
                        } else if (isThrownException(encodedResponse)) {
                            caught = (Throwable) createStreamReader(encodedResponse).readObject();
                        } else {
                            caught = new InvocationException(encodedResponse);
                        }
                    } catch (SerializationException ex) {
                        caught = new IncompatibleRemoteServiceException("The response could not be deserialized", ex);
                    } catch (Throwable ex) {
                        caught = ex;
                    } finally {
                        boolean toss = statsContext.isStatsAvailable()
                                && statsContext.stats(statsContext.timeStat(methodName, "responseDeserialized"));
                    }

                    try {
                        if (caught == null) {
                            callback.onSuccess(result);
                        } else {
                            callback.onFailure(caught);
                        }
                    } finally {
                        Object returned = (caught == null) ? result : caught;
                        boolean toss = statsContext.isStatsAvailable() && statsContext.stats(statsContext.timeStat(methodName, returned, "end"));
                    }
                }

                public void onTimeout() {
                    InvocationException iex = new InvocationException("JSONP request timed out after "
                            + Integer.toString(requestInvoker.getTimeoutMillis()) + " ms");

                    try {
                        callback.onFailure(iex);
                    } finally {
                        boolean toss = statsContext.isStatsAvailable() && statsContext.stats(statsContext.timeStat(methodName, iex, "end"));
                    }
                }

            });
        } catch (Exception ex) {
            InvocationException iex = new InvocationException("Unable to initiate the asynchronous service invocation -- check the network connection", ex);
            callback.onFailure(iex);
        } finally {
            if (statsContext.isStatsAvailable()) {
                statsContext.stats(statsContext.bytesStat(methodName, requestData.length(), "requestSent"));
            }
        }

        return null;
    }

    // following code is copied from RemoteServiceProxy

    static boolean isReturnValue(String encodedResponse) {
        return encodedResponse.startsWith("//OK");
    }

    static boolean isThrownException(String encodedResponse) {
        return encodedResponse.startsWith("//EX");
    }

}
